package com.bee2c.pub.util.dao;

import static org.springframework.data.jpa.repository.query.QueryUtils.toOrders;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.Assert;

public class BaseDao<T> {

	@PersistenceContext
	protected EntityManager em;

	@SuppressWarnings("unchecked")
	protected <K> List<K> findAll(String jql, Map<String, Object> paraMap) {
		Query query = em.createQuery(jql.toString());
		if (paraMap != null) {
			for (String key : paraMap.keySet()) {
				query.setParameter(key, paraMap.get(key));
			}
		}
		return query.getResultList();
	}

	protected List<T> findAll(Specification<T> spec, Sort sort) {
		return getQuery(spec, sort).getResultList();
	}

	protected List<T> findAll(Specification<T> spec) {
		return getQuery(spec, (Sort) null).getResultList();
	}

	protected Page<T> findAll(Specification<T> spec, Sort sort, int pageIndex, int pageSize) {
		Pageable pageable = new PageRequest(pageIndex - 1, pageSize, sort);
		Class<T> domainClass = getDomainClass();
		TypedQuery<T> query = getQuery(spec, pageable.getSort());
		return readPage(query, domainClass, pageable, spec);
	}

	protected Page<T> readPage(TypedQuery<T> query, Class<T> domainClass, Pageable pageable, Specification<T> spec) {

		query.setFirstResult(pageable.getOffset());
		query.setMaxResults(pageable.getPageSize());

		Long total = executeCountQuery(getCountQuery(spec, domainClass));
		List<T> content = total > pageable.getOffset() ? query.getResultList() : Collections.<T> emptyList();

		return new PageImpl<T>(content, pageable, total);
	}

	private static Long executeCountQuery(TypedQuery<Long> query) {
		Assert.notNull(query, "query can't be null");
		List<Long> totals = query.getResultList();
		Long total = 0L;
		for (Long element : totals) {
			total += element == null ? 0 : element;
		}
		return total;
	}

	protected <S extends T> TypedQuery<Long> getCountQuery(Specification<S> spec, Class<S> domainClass) {

		CriteriaBuilder builder = em.getCriteriaBuilder();
		CriteriaQuery<Long> query = builder.createQuery(Long.class);

		Root<S> root = applySpecificationToCriteria(spec, domainClass, query);

		if (query.isDistinct()) {
			query.select(builder.countDistinct(root));
		} else {
			query.select(builder.count(root));
		}

		query.orderBy(Collections.<Order> emptyList());

		return em.createQuery(query);
	}

	protected TypedQuery<T> getQuery(Specification<T> spec, Sort sort) {
		Class<T> domainClass = getDomainClass();
		CriteriaBuilder builder = em.getCriteriaBuilder();
		CriteriaQuery<T> query = builder.createQuery(domainClass);

		Root<T> root = applySpecificationToCriteria(spec, domainClass, query);
		query.select(root);

		if (sort != null) {
			query.orderBy(toOrders(sort, root, builder));
		}

		return em.createQuery(query);
	}

	private <S, U extends T> Root<U> applySpecificationToCriteria(Specification<U> spec, Class<U> domainClass,
			CriteriaQuery<S> query) {

		Assert.notNull(query, "query can't be null");
		Assert.notNull(domainClass, "domainClass can't be null");
		Root<U> root = query.from(domainClass);

		if (spec == null) {
			return root;
		}

		CriteriaBuilder builder = em.getCriteriaBuilder();
		Predicate predicate = spec.toPredicate(root, query, builder);

		if (predicate != null) {
			query.where(predicate);
		}

		return root;
	}

	@SuppressWarnings("unchecked")
	private Class<T> getDomainClass() {
		Type type = getClass().getGenericSuperclass();
		if (type instanceof ParameterizedType) {
			return (Class<T>) ((ParameterizedType) type).getActualTypeArguments()[0];
		}
		return null;
	}

}
